/* * Copyright 2015 Ben Manes. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.github.benmanes.caffeine.testing; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.is; import static org.testng.Assert.fail; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Queue; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiConsumer; import java.util.stream.Collectors; import java.util.stream.IntStream; import org.testng.log4testng.Logger; import com.google.common.base.Throwables; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.ThreadFactoryBuilder; /** * Shared utilities for multi-threaded tests. * * @author ben.manes@gmail.com (Ben Manes) */ public final class Threads { private static final Logger logger = Logger.getLogger(Threads.class); public static final int ITERATIONS = 40000; public static final int NTHREADS = 20; public static final int TIMEOUT = 30; private Threads() {} public static <A> void runTest(A collection, List<BiConsumer<A, Integer>> operations) { Queue<String> failures = new ConcurrentLinkedQueue<>(); Runnable thrasher = new Thrasher<A>(collection, failures, operations); Threads.executeWithTimeOut(failures, () -> ConcurrentTestHarness.timeTasks(Threads.NTHREADS, thrasher)); assertThat(failures, is(empty())); } public static void executeWithTimeOut(Queue<String> failures, Callable<Long> task) { ExecutorService es = Executors.newSingleThreadExecutor( new ThreadFactoryBuilder().setDaemon(true).build()); Future<Long> future = es.submit(task); try { long timeNS = future.get(TIMEOUT, TimeUnit.SECONDS); logger.debug("\nExecuted in " + TimeUnit.NANOSECONDS.toSeconds(timeNS) + " second(s)"); } catch (ExecutionException e) { fail("Exception during test: " + e.toString(), e); } catch (TimeoutException e) { handleTimout(failures, es, e); } catch (InterruptedException e) { fail("", e); } } public static void handleTimout(Queue<String> failures, ExecutorService es, TimeoutException e) { for (StackTraceElement[] trace : Thread.getAllStackTraces().values()) { for (StackTraceElement element : trace) { logger.info("\tat " + element); } if (trace.length > 0) { logger.info("------"); } } MoreExecutors.shutdownAndAwaitTermination(es, 10, TimeUnit.SECONDS); for (String failure : failures) { logger.debug(failure); } fail("Spun forever", e); } public static List<List<Integer>> workingSets(int nThreads, int iterations) { List<Integer> keys = IntStream.range(0, iterations).boxed() .map(i -> ThreadLocalRandom.current().nextInt(iterations / 100)) .collect(Collectors.toList()); return shuffle(nThreads, keys); } /** * Based on the passed in working set, creates N shuffled variants. * * @param samples the number of variants to create * @param baseline the base working set to build from */ private static <T> List<List<T>> shuffle(int samples, Collection<T> baseline) { List<List<T>> workingSets = new ArrayList<>(samples); for (int i = 0; i < samples; i++) { List<T> workingSet = new ArrayList<>(baseline); Collections.shuffle(workingSet); workingSets.add(ImmutableList.copyOf(workingSet)); } return ImmutableList.copyOf(workingSets); } /** Executes operations against the cache to simulate random load. */ public static final class Thrasher<A> implements Runnable { private final List<BiConsumer<A, Integer>> operations; private final List<List<Integer>> sets; private final Queue<String> failures; private final AtomicInteger index; private final A collection; public Thrasher(A collection, Queue<String> failures, List<BiConsumer<A, Integer>> operations) { this.sets = workingSets(Threads.NTHREADS, Threads.ITERATIONS); this.index = new AtomicInteger(); this.operations = operations; this.collection = collection; this.failures = failures; } @Override public void run() { int id = index.getAndIncrement(); for (Integer e : sets.get(id)) { BiConsumer<A, Integer> operation = operations.get( ThreadLocalRandom.current().nextInt(operations.size())); try { operation.accept(collection, e); } catch (Throwable t) { failures.add(String.format("Failed: key %s on operation %s%n%s", e, operation, Throwables.getStackTraceAsString(t))); throw t; } } } } }